{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 概述\n",
    "\n",
    "上一节我们使用“横纵式”教学法中的纵向极简方案快速完成手写数字识别任务的建模，但模型测试效果并未达成预期。我们换个思路，从横向展开，如 **图1** 所示，逐个环节优化，以达到最优训练效果。本节主要介绍手写数字识别模型中，数据处理的优化方法。\n",
    "\n",
    "<center><img src=\"https://ai-studio-static-online.cdn.bcebos.com/9bdee67f18a14879b2ddbbd81aca54973120c6a7be10403a9b983afd28b5c97d\" width=\"800\" hegiht=\"\" ></center>\n",
    "<center><br>图1：“横纵式”教学法 — 数据处理优化 </br></center>\n",
    "<br></br>\n",
    "\n",
    "上一节，我们通过调用飞桨提供的API（[paddle.dataset.mnist](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/data/dataset_cn.html)）加载MNIST数据集。但在工业实践中，我们面临的任务和数据环境千差万别，通常需要自己编写适合当前任务的数据处理程序，一般涉及如下五个环节：\n",
    "\n",
    "* 读入数据\n",
    "* 划分数据集\n",
    "* 生成批次数据\n",
    "* 训练样本集乱序\n",
    "* 校验数据有效性\n",
    "\n",
    "### 前提条件\n",
    "\n",
    "在数据读取与处理前，首先要加载飞桨平台和数据处理库，代码如下。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "#数据处理部分之前的代码，加入部分数据处理的库\n",
    "import paddle\n",
    "import paddle.fluid as fluid\n",
    "from paddle.fluid.dygraph.nn import Linear\n",
    "import numpy as np\n",
    "import os\n",
    "import gzip\n",
    "import json\n",
    "import random"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 导入数据处理库\r\n",
    "import paddle\r\n",
    "import paddle.fluid as fluid\r\n",
    "from paddle.fluid.dygraph.nn import Linear\r\n",
    "import numpy as np\r\n",
    "import os\r\n",
    "import gzip  # 解压函数\r\n",
    "import json\r\n",
    "import random\r\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 读入数据并划分数据集\n",
    "\n",
    "在实际应用中，保存到本地的数据存储格式多种多样，如MNIST数据集以json格式存储在本地，其数据存储结构如 **图2** 所示。\n",
    "\n",
    "<center><img src=\"https://ai-studio-static-online.cdn.bcebos.com/9ae4d15f6920472b95c5600484748b5808b8b0864b074ae490ea512a8dcf1ca2\" width=\"400\" hegiht=\"\" ></center>\n",
    "<center><br>图2：MNIST数据集的存储结构</br></center>\n",
    "<br></br>\n",
    "\n",
    "**data**包含三个元素的列表：**train_set**、**val_set**、 **test_set**。\n",
    "\n",
    "* **train_set（训练集）**：包含50000条手写数字图片和对应的标签，用于确定模型参数。\n",
    "* **val_set（验证集）**：包含10000条手写数字图片和对应的标签，用于调节模型超参数（如多个网络结构、正则化权重的最优选择）。\n",
    "* **test_set（测试集）**：包含10000条手写数字图片和对应的标签，用于估计应用效果（没有在模型中应用过的数据，更贴近模型在真实场景应用的效果）。\n",
    "\n",
    "**train_set**包含两个元素的列表：**train_images**、**train_labels**。\n",
    "\n",
    "* **train_imgs**：[5000, 784]的二维列表，包含5000张图片。每张图片用一个长度为784的向量表示，内容是28\\*28尺寸的像素灰度值（黑白图片）。\n",
    "* **train_labels**：[5000, ]的列表，表示这些图片对应的分类标签，即0-9之间的一个数字。\n",
    "\n",
    "在本地'./work/'目录下读取文件名称为'mnist.json.gz'的MINST数据，并拆分成训练集、验证集和测试集，代码如下所示。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loading mnist dataset from ./work/mnist.json.gz ......\n",
      "mnist dataset load done\n",
      "验证数据集数量:  10000\n",
      "测试数据集数量:  10000\n"
     ]
    }
   ],
   "source": [
    "# 声明数据集文件位置\n",
    "datafile = './work/mnist.json.gz'\n",
    "print('loading mnist dataset from {} ......'.format(datafile))\n",
    "# 加载json数据文件\n",
    "data = json.load(gzip.open(datafile))\n",
    "print('mnist dataset load done')\n",
    "# 读取到的数据区分训练集，验证集，测试集\n",
    "train_set, val_set, eval_set = data\n",
    "\n",
    "# 数据集相关参数，图片高度IMG_ROWS, 图片宽度IMG_COLS\n",
    "IMG_ROWS = 28\n",
    "IMG_COLS = 28\n",
    "\n",
    "# 打印数据信息\n",
    "imgs, labels = train_set[0], train_set[1]\n",
    "\n",
    "# 观察验证集数量\n",
    "imgs, labels = val_set[0], val_set[1]\n",
    "print(\"验证数据集数量: \", len(imgs))\n",
    "\n",
    "# 观察测试集数量\n",
    "imgs, labels = val= eval_set[0], eval_set[1]\n",
    "print(\"测试数据集数量: \", len(imgs))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loading mnist dataset from ./work/minist.json.gz ......\n"
     ]
    },
    {
     "ename": "FileNotFoundError",
     "evalue": "[Errno 2] No such file or directory: './work/minist.json.gz'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-4-bade4333844d>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      3\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'loading mnist dataset from {} ......'\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdatafile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;31m# 加载json数据文件\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mjson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mload\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgzip\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdatafile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      6\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'mnist dataset load done'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      7\u001b[0m \u001b[0;31m# 读取到的数据区分训练集，验证集，测试集\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;32m/opt/conda/envs/python35-paddle120-env/lib/python3.7/gzip.py\u001b[0m in \u001b[0;36mopen\u001b[0;34m(filename, mode, compresslevel, encoding, errors, newline)\u001b[0m\n\u001b[1;32m     51\u001b[0m     \u001b[0mgz_mode\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreplace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"t\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     52\u001b[0m     \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbytes\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mos\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPathLike\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 53\u001b[0;31m         \u001b[0mbinary_file\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mGzipFile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgz_mode\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcompresslevel\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     54\u001b[0m     \u001b[0;32melif\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"read\"\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"write\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     55\u001b[0m         \u001b[0mbinary_file\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mGzipFile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgz_mode\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcompresslevel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfilename\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;32m/opt/conda/envs/python35-paddle120-env/lib/python3.7/gzip.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, filename, mode, compresslevel, fileobj, mtime)\u001b[0m\n\u001b[1;32m    161\u001b[0m             \u001b[0mmode\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;34m'b'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    162\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mfileobj\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 163\u001b[0;31m             \u001b[0mfileobj\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmyfileobj\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mbuiltins\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmode\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;34m'rb'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    164\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0mfilename\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    165\u001b[0m             \u001b[0mfilename\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgetattr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfileobj\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'name'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: './work/minist.json.gz'"
     ]
    }
   ],
   "source": [
    "# 声明数据集文件的位置\r\n",
    "datafile = './work/minist.json.gz'\r\n",
    "print('loading mnist dataset from {} ......'.format(datafile))\r\n",
    "# 加载json数据文件\r\n",
    "data = json.load(gzip.open(datafile))\r\n",
    "print('mnist dataset load done')\r\n",
    "# 读取到的数据区分训练集，验证集，测试集\r\n",
    "train_set, val_set, eval_set = data\r\n",
    "# 数据集相关参数，图片高度IMG_ROWS, 图片宽度IMG_COLS\r\n",
    "IMG_ROWS = 28\r\n",
    "IMG_COLS = 28\r\n",
    "# 打印数据信息\r\n",
    "imgs, labels = train_set[0], train_set[1]\r\n",
    "print('训练数据集数量：', len(imgs))\r\n",
    "# 观察验证集数量\r\n",
    "imgs, labels = val_set[0], val_set[1]\r\n",
    "print(\"验证数据集数量: \", len(imgs))\r\n",
    "# 观察测试集数量\r\n",
    "imgs, labels = eval_set[0], eval_set[1]\r\n",
    "print(\"测试数据集数量: \", len(imgs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "## 扩展阅读：为什么学术界的模型总在不断精进呢？\n",
    "\n",
    "通常某组织发布一个新任务的训练集和测试集数据后，全世界的科学家都针对该数据集进行创新研究，随后大量针对该数据集的论文会陆续发表。论文1的A模型声称在测试集的准确率70%，论文2的B模型声称在测试集的准确率提高到72%，论文N的X模型声称在测试集的准确率提高到90% ...\n",
    "\n",
    "然而这些论文中的模型在测试集上准确率提升真实有效么？我们不妨大胆猜测一下。\n",
    "\n",
    "假设所有论文共产生1000个模型，这些模型使用的是测试数据集来评判模型效果，并最终选出效果最优的模型。这相当于把原始的测试集当作了验证集，使得测试集失去了真实评判模型效果的能力，正如机器学习领域非常流行的一句话：“拷问数据足够久，它终究会招供”。\n",
    "\n",
    "<center><img src=\"https://ai-studio-static-online.cdn.bcebos.com/bd84aa9712a4428c91116c477ca348721cc68effc32a49d49ff14d47032879c1\" width=\"200\" hegiht=\"\" ></center>\n",
    "<center><br>图3：拷问数据足够久，它总会招供</br></center>\n",
    "<br></br>\n",
    "\n",
    "那么当我们需要将学术界研发的模型复用于工业项目时，应该如何选择呢？给读者一个小建议：当几个模型的准确率在测试集上差距不大时，尽量选择网络结构相对简单的模型。往往越精巧设计的模型和方法，越不容易在不同的数据集之间迁移。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 训练样本乱序、生成批次数据\n",
    "\n",
    "* **训练样本乱序：** 先将样本按顺序进行编号，建立ID集合index_list。然后将index_list乱序，最后按乱序后的顺序读取数据。\n",
    "\n",
    "------\n",
    "**说明：**\n",
    "\n",
    "通过大量实验发现，模型对最后出现的数据印象更加深刻。训练数据导入后，越接近模型训练结束，最后几个批次数据对模型参数的影响越大。为了避免模型记忆影响训练效果，需要进行样本乱序操作。\n",
    "\n",
    "------\n",
    "* **生成批次数据：** 先设置合理的batch_size，再将数据转变成符合模型输入要求的np.array格式返回。同时，在返回数据时将Python生成器设置为``yield``模式，以减少内存占用。\n",
    "\n",
    "在执行如上两个操作之前，需要先将数据处理代码封装成load_data函数，方便后续调用。load_data有三种模型：``train``、``valid``、``eval``，分为对应返回的数据是训练集、验证集、测试集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "imgs, labels = train_set[0], train_set[1]\n",
    "print(\"训练数据集数量: \", len(imgs))\n",
    "# 获得数据集长度\n",
    "imgs_length = len(imgs)\n",
    "# 定义数据集每个数据的序号，根据序号读取数据\n",
    "index_list = list(range(imgs_length))\n",
    "# 读入数据时用到的批次大小\n",
    "BATCHSIZE = 100\n",
    "\n",
    "# 随机打乱训练数据的索引序号\n",
    "random.shuffle(index_list)\n",
    "\n",
    "# 定义数据生成器，返回批次数据\n",
    "def data_generator():\n",
    "\n",
    "    imgs_list = []\n",
    "    labels_list = []\n",
    "    for i in index_list:\n",
    "        # 将数据处理成希望的格式，比如类型为float32，shape为[1, 28, 28]\n",
    "        img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')\n",
    "        label = np.reshape(labels[i], [1]).astype('float32')\n",
    "        imgs_list.append(img) \n",
    "        labels_list.append(label)\n",
    "        if len(imgs_list) == BATCHSIZE:\n",
    "            # 获得一个batchsize的数据，并返回\n",
    "            yield np.array(imgs_list), np.array(labels_list)\n",
    "            # 清空数据读取列表\n",
    "            imgs_list = []\n",
    "            labels_list = []\n",
    "\n",
    "    # 如果剩余数据的数目小于BATCHSIZE，\n",
    "    # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch\n",
    "    if len(imgs_list) > 0:\n",
    "        yield np.array(imgs_list), np.array(labels_list)\n",
    "    return data_generator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "imgs, labels = train_set[0], train_set[1]\r\n",
    "print(\"训练数据集数量: \", len(imgs))\r\n",
    "# 获得数据集长度\r\n",
    "imgs_length = len(imgs)\r\n",
    "# 定义数据集每个数据的序号，根据序号读取数据\r\n",
    "index_list = list(range(imgs_length))\r\n",
    "# 读入数据时用到的批次大小\r\n",
    "BATCHSIZE = 100\r\n",
    "# 随机打乱训练数据的索引序号\r\n",
    "random.shuffle(index_list)\r\n",
    "\r\n",
    "# 定义数据生成器，返回批次数据\r\n",
    "# 返回每次随机排序的全部数据，这些全部批次按BATCHSIZE分批返回\r\n",
    "def data_generator():\r\n",
    "    imgs_list = []\r\n",
    "    labels_list = []\r\n",
    "    for i in index_list:\r\n",
    "        # 将数据处理成希望的格式，比如类型为float32，shape为[1, 28, 28]\r\n",
    "        img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')\r\n",
    "        label = np.reshape(labels[i], [1]).astype('float32')\r\n",
    "        # print(img, label)    #img为一张图片1*28*28  label (1,)内容[2.]\r\n",
    "        imgs_list.append(img)  # 定义图片的列表\r\n",
    "        labels_list.append(label)  # 定义标签的列表\r\n",
    "        if len(imgs_list) == BATCHSIZE:\r\n",
    "            # 获得一个batchsize的数据，并返回\r\n",
    "            yield np.array(imgs_list), np.array(labels_list)\r\n",
    "            # 清空数据读取列表\r\n",
    "            imgs_list = []\r\n",
    "            labels_list = []\r\n",
    "    # 如果剩余数据的数目小于BATCHSIZE，\r\n",
    "    # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch\r\n",
    "    if len(imgs_list) > 0:\r\n",
    "        yield np.array(imgs_list), np.array(labels_list)\r\n",
    "    return data_generator\r\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 声明数据读取函数，从训练集中读取数据\n",
    "train_loader = data_generator\n",
    "# 以迭代的形式读取数据\n",
    "for batch_id, data in enumerate(train_loader()):\n",
    "    image_data, label_data = data\n",
    "    if batch_id == 0:\n",
    "        # 打印数据shape和类型\n",
    "        print(\"打印第一个batch数据的维度:\")\n",
    "        print(\"图像维度: {}, 标签维度: {}\".format(image_data.shape, label_data.shape)) \n",
    "        # 每一个batchsize图像维度: (100, 1, 28, 28), 标签维度: (100, 1)\n",
    "        # 说明每一个batchsize有100张图片，每张图片的大小是28*28\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 声明数据读取函数，从训练集中读取数据\r\n",
    "train_loader = data_generator\r\n",
    "# 以迭代的形式读取数据\r\n",
    "for batch_id, data in enumerate(train_loader()):  # train_loader()每次返回的一个批次的数据 BATCHSIZE = 100\r\n",
    "    image_data, label_data = data\r\n",
    "    if batch_id == 0:\r\n",
    "        # 打印数据的shape和类型\r\n",
    "        print(\"打印第一个batch数据的维度:\")\r\n",
    "        print(\"图像维度: {}, 标签维度: {}\".format(image_data.shape, label_data.shape))\r\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 校验数据有效性\n",
    "\n",
    "在实际应用中，原始数据可能存在标注不准确、数据杂乱或格式不统一等情况。因此在完成数据处理流程后，还需要进行数据校验，一般有两种方式：\n",
    "\n",
    "* 机器校验：加入一些校验和清理数据的操作。\n",
    "* 人工校验：先打印数据输出结果，观察是否是设置的格式。再从训练的结果验证数据处理和读取的有效性。\n",
    "\n",
    "##  机器校验\n",
    "\n",
    "如下代码所示，如果数据集中的图片数量和标签数量不等，说明数据逻辑存在问题，可使用assert语句校验图像数量和标签数据是否一致。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "    imgs_length = len(imgs)\n",
    "\n",
    "    assert len(imgs) == len(labels), \\\n",
    "          \"length of train_imgs({}) should be the same as train_labels({})\".format(len(imgs), len(label))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "imgs_length = len(imgs)\r\n",
    "\r\n",
    "assert len(imgs) == len(labels), \"length of train_imgs({}) should be the same as train_labels({})\".format(len(imgs), len(label))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "## 人工校验\n",
    "\n",
    "人工校验是指打印数据输出结果，观察是否是预期的格式。实现数据处理和加载函数后，我们可以调用它读取一次数据，观察数据的shape和类型是否与函数中设置的一致。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 声明数据读取函数，从训练集中读取数据\n",
    "train_loader = data_generator\n",
    "# 以迭代的形式读取数据\n",
    "for batch_id, data in enumerate(train_loader()):\n",
    "    image_data, label_data = data\n",
    "    if batch_id == 0:\n",
    "        # 打印数据shape和类型\n",
    "        print(\"打印第一个batch数据的维度，以及数据的类型:\")\n",
    "        print(\"图像维度: {}, 标签维度: {}, 图像数据类型: {}, 标签数据类型: {}\".format(image_data.shape, label_data.shape, type(image_data), type(label_data)))\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 封装数据读取与处理函数\n",
    "\n",
    "上文，我们从读取数据，划分数据集，到打乱训练数据，构建数据读取器以及数据数据校验，完成了一整套一般性的数据处理流程，下面将这些步骤放在一个函数中实现，方便在神经网络训练时直接调用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def load_data(mode='train'):\n",
    "    datafile = './work/mnist.json.gz'\n",
    "    print('loading mnist dataset from {} ......'.format(datafile))\n",
    "    # 加载json数据文件\n",
    "    data = json.load(gzip.open(datafile))\n",
    "    print('mnist dataset load done')\n",
    "   \n",
    "    # 读取到的数据区分训练集，验证集，测试集\n",
    "    train_set, val_set, eval_set = data\n",
    "    if mode=='train':\n",
    "        # 获得训练数据集\n",
    "        imgs, labels = train_set[0], train_set[1]\n",
    "    elif mode=='valid':\n",
    "        # 获得验证数据集\n",
    "        imgs, labels = val_set[0], val_set[1]\n",
    "    elif mode=='eval':\n",
    "        # 获得测试数据集\n",
    "        imgs, labels = eval_set[0], eval_set[1]\n",
    "    else:\n",
    "        raise Exception(\"mode can only be one of ['train', 'valid', 'eval']\")\n",
    "    print(\"训练数据集数量: \", len(imgs))\n",
    "    \n",
    "    # 校验数据\n",
    "    imgs_length = len(imgs)\n",
    "\n",
    "    assert len(imgs) == len(labels), \\\n",
    "          \"length of train_imgs({}) should be the same as train_labels({})\".format(len(imgs), len(label))\n",
    "    \n",
    "    # 获得数据集长度\n",
    "    imgs_length = len(imgs)\n",
    "    \n",
    "    # 定义数据集每个数据的序号，根据序号读取数据\n",
    "    index_list = list(range(imgs_length))\n",
    "    # 读入数据时用到的批次大小\n",
    "    BATCHSIZE = 100\n",
    "    \n",
    "    # 定义数据生成器\n",
    "    def data_generator():\n",
    "        if mode == 'train':\n",
    "            # 训练模式下打乱数据\n",
    "            random.shuffle(index_list)\n",
    "        imgs_list = []\n",
    "        labels_list = []\n",
    "        for i in index_list:\n",
    "            # 将数据处理成希望的格式，比如类型为float32，shape为[1, 28, 28]\n",
    "            img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')\n",
    "            label = np.reshape(labels[i], [1]).astype('float32')\n",
    "            imgs_list.append(img) \n",
    "            labels_list.append(label)\n",
    "            if len(imgs_list) == BATCHSIZE:\n",
    "                # 获得一个batchsize的数据，并返回\n",
    "                yield np.array(imgs_list), np.array(labels_list)\n",
    "                # 清空数据读取列表\n",
    "                imgs_list = []\n",
    "                labels_list = []\n",
    "    \n",
    "        # 如果剩余数据的数目小于BATCHSIZE，\n",
    "        # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch\n",
    "        if len(imgs_list) > 0:\n",
    "            yield np.array(imgs_list), np.array(labels_list)\n",
    "    return data_generator"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "下面定义一层神经网络，利用定义好的数据处理函数，完成神经网络的训练。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "#数据处理部分之后的代码，数据读取的部分调用Load_data函数\n",
    "# 定义网络结构，同上一节所使用的网络结构\n",
    "class MNIST(fluid.dygraph.Layer):\n",
    "    def __init__(self, name_scope):\n",
    "        super(MNIST, self).__init__(name_scope)\n",
    "        self.fc = Linear(input_dim=784, output_dim=1, act=None)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        inputs = fluid.layers.reshape(inputs, (-1, 784))\n",
    "        outputs = self.fc(inputs)\n",
    "        return outputs\n",
    "\n",
    "# 训练配置，并启动训练过程\n",
    "with fluid.dygraph.guard():\n",
    "    model = MNIST(\"mnist\")\n",
    "    model.train()\n",
    "    #调用加载数据的函数\n",
    "    train_loader = load_data('train')\n",
    "    optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001, parameter_list=model.parameters())\n",
    "    EPOCH_NUM = 10\n",
    "    for epoch_id in range(EPOCH_NUM):\n",
    "        for batch_id, data in enumerate(train_loader()):\n",
    "            #准备数据，变得更加简洁\n",
    "            image_data, label_data = data  # 每一个batch为BATCHSIZE = 100大小的数据量\n",
    "            image = fluid.dygraph.to_variable(image_data)  # 数据类型转变成paddle框架支持的框架\n",
    "            label = fluid.dygraph.to_variable(label_data)\n",
    "            \n",
    "            #前向计算的过程\n",
    "            predict = model(image)  # 这边直接调用model他会调用里面的froward函数\n",
    "            \n",
    "            #计算损失，取一个批次样本损失的平均值\n",
    "            loss = fluid.layers.square_error_cost(predict, label) # 均方误差\n",
    "            avg_loss = fluid.layers.mean(loss)\n",
    "            \n",
    "            #每训练了200批次的数据，打印下当前Loss的情况\n",
    "            if batch_id % 200 == 0:\n",
    "                print(\"epoch: {}, batch: {}, loss is: {}\".format(epoch_id, batch_id, avg_loss.numpy()))\n",
    "            \n",
    "            #后向传播，更新参数的过程\n",
    "            avg_loss.backward()\n",
    "            optimizer.minimize(avg_loss)\n",
    "            model.clear_gradients()  # 这边每次更新完参数都需要清理一下梯度参数\n",
    "\n",
    "    #保存模型参数\n",
    "    fluid.save_dygraph(model.state_dict(), 'mnist')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "\n",
    "# 异步数据读取\n",
    "\n",
    "上面提到的数据读取采用的是同步数据读取方式。对于样本量较大、数据读取较慢的场景，建议采用异步数据读取方式。异步读取数据时，数据读取和模型训练并行执行，从而加快了数据读取速度，牺牲一小部分内存换取数据读取效率的提升，二者关系如 **图4** 所示。\n",
    "\n",
    "<center><img src=\"https://ai-studio-static-online.cdn.bcebos.com/c0342c5f680442f99628c81ad29924e6e2a325ef9d764db8989b7bd6fbe001cb\" width=\"600\" ></center>\n",
    "<center><br>图4：同步数据读取和异步数据读取示意图</br></center>\n",
    "<br></br>\n",
    "\n",
    "* **同步数据读取**：数据读取与模型训练串行。当模型需要数据时，才运行数据读取函数获得当前批次的数据。在读取数据期间，模型一直等待数据读取结束才进行训练，数据读取速度相对较慢。\n",
    "* **异步数据读取**：数据读取和模型训练并行。读取到的数据不断的放入缓存区，无需等待模型训练就可以启动下一轮数据读取。当模型训练完一个批次后，不用等待数据读取过程，直接从缓存区获得下一批次数据进行训练，从而加快了数据读取速度。\n",
    "* **异步队列**：数据读取和模型训练交互的仓库，二者均可以从仓库中读取数据，它的存在使得两者的工作节奏可以解耦。\n",
    "\n",
    "使用飞桨实现异步数据读取非常简单，代码如下所示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 定义数据读取后存放的位置，CPU或者GPU，这里使用CPU\n",
    "# place = fluid.CUDAPlace(0) 时，数据读到GPU上\n",
    "place = fluid.CPUPlace()\n",
    "with fluid.dygraph.guard(place):\n",
    "    # 声明数据加载函数，使用训练模式\n",
    "    train_loader = load_data(mode='train')\n",
    "    # 定义DataLoader对象用于加载Python生成器产生的数据\n",
    "    data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True)\n",
    "    # 设置数据生成器\n",
    "    data_loader.set_batch_generator(train_loader, places=place)\n",
    "    # 迭代的读取数据并打印数据的形状\n",
    "    for i, data in enumerate(data_loader):\n",
    "        image_data, label_data = data\n",
    "        print(i, image_data.shape, label_data.shape)\n",
    "        if i>=5:\n",
    "            break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# 定义数据读取后存放的位置，CPU或者GPU，这里使用CPU\r\n",
    "# place = fluid.CUDAPlace(0)时，数据读到GPU上面\r\n",
    "place = fluid.CPUPlace()\r\n",
    "# 声明作用域，在什么环境下运行\r\n",
    "with fluid.dygraph.guard(place):\r\n",
    "    # 声明数据加载函数，使用训练模式\r\n",
    "    train_loader = load_data(mode='train')\r\n",
    "    # 定义异步\r\n",
    "    # 定义DataLoader对象用于加载Python生成器产生的数据\r\n",
    "    data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True)  # capacity=5仓库的容量为5\r\n",
    "    # 设置数据生成器\r\n",
    "    data_loader.set_batch_generator(train_loader, places=place)  # 把用python创建的数据生成器导入进来\r\n",
    "    for i, data in enumerate(data_loader):\r\n",
    "        image_data, label_data = data\r\n",
    "        print(i, image_data.shape, label_data.shape)\r\n",
    "        if i > 5:\r\n",
    "            break\r\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "与同步数据读取相比，异步数据读取仅增加了三行代码，如下所示。\n",
    "\n",
    "```python\n",
    "place = fluid.CPUPlace()\n",
    "\n",
    "# 设置读取的数据是放在CPU还是GPU上。\n",
    "\n",
    "data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True) \n",
    "\n",
    "# 创建一个DataLoader对象用于加载Python生成器产生的数据。数据会由Python线程预先读取，并异步送入一个队列中。\n",
    "\n",
    "data_loader.set_batch_generator(train_loader, place) \n",
    "\n",
    "# 用创建的DataLoader对象设置一个数据生成器set_batch_generator，输入的参数是一个Python数据生成器train_loader和服务器资源类型place（标明CPU还是GPU）\n",
    "```\n",
    "\n",
    "fluid.io.DataLoader.from_generator参数名称、参数含义、默认值如下：\n",
    "\n",
    "* 参数含义如下：\n",
    "\n",
    "  - feed_list        仅在paddle静态图中使用，动态图中设置为None，本教程默认使用动态图的建模方式。\n",
    "  - capacity        表示在DataLoader中维护的队列容量，如果读取数据的速度很快，建议设置为更大的值。\n",
    "  - use_double_buffer   是一个布尔型的参数，设置为True时Dataloader会预先异步读取下一个batch的数据放到缓存区。\n",
    "  - iterable          表示创建的Dataloader对象是否是可迭代的，一般设置为True。\n",
    "  - return_list        在动态图模式下需要设置为True，静态图模式下设置为False。\n",
    "\n",
    "* 参数名和默认值如下：\n",
    "\n",
    "  - feed_list=None, \n",
    "  - capacity=None, \n",
    "  - use_double_buffer=True,\n",
    "  - iterable=True,\n",
    "  - return_list=False\n",
    "\n",
    "异步数据读取并训练的完整案例代码如下所示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "with fluid.dygraph.guard():\n",
    "    model = MNIST(\"mnist\")\n",
    "    model.train()\n",
    "    #调用加载数据的函数\n",
    "    train_loader = load_data('train')\n",
    "    # 创建异步数据读取器\n",
    "    place = fluid.CPUPlace()\n",
    "    data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True)\n",
    "    data_loader.set_batch_generator(train_loader, places=place)\n",
    "    \n",
    "    optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001, parameter_list=model.parameters())\n",
    "    EPOCH_NUM = 3\n",
    "    for epoch_id in range(EPOCH_NUM):\n",
    "        for batch_id, data in enumerate(data_loader):\n",
    "            image_data, label_data = data\n",
    "            image = fluid.dygraph.to_variable(image_data)\n",
    "            label = fluid.dygraph.to_variable(label_data)\n",
    "            \n",
    "            predict = model(image)\n",
    "            \n",
    "            loss = fluid.layers.square_error_cost(predict, label)\n",
    "            avg_loss = fluid.layers.mean(loss)\n",
    "            \n",
    "            if batch_id % 200 == 0:\n",
    "                print(\"epoch: {}, batch: {}, loss is: {}\".format(epoch_id, batch_id, avg_loss.numpy()))\n",
    "            \n",
    "            avg_loss.backward()\n",
    "            optimizer.minimize(avg_loss)\n",
    "            model.clear_gradients()\n",
    "\n",
    "    fluid.save_dygraph(model.state_dict(), 'mnist')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "从异步数据读取的训练结果来看，损失函数下降与同步数据读取训练结果一致。注意，异步读取数据只在数据量规模巨大时会带来显著的性能提升，对于多数场景采用同步数据读取的方式已经足够。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "PaddlePaddle 1.7.0 (Python 3.5)",
   "language": "python",
   "name": "py35-paddle1.2.0"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
